﻿// Copyright (c) Microsoft Corporation
//
// All rights reserved. 
//
// Licensed under the Apache License, Version 2.0 (the ""License""); you may not use this file 
// except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING 
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY 
// OR NON-INFRINGEMENT. 
//
// See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Blob;
using TED.WindowsAzure.Unplugged.Mobile.Exceptions;
using TED.WindowsAzure.Unplugged.Mobile.File;
using TED.WindowsAzure.Unplugged.Mobile.Folder;
using TED.WindowsAzure.Unplugged.Mobile.Interfaces;
using TED.WindowsAzure.Unplugged.Mobile.RemoteStorage.AzureBlob.Extensions;
using TED.WindowsAzure.Unplugged.Mobile.Storage;
using Microsoft.WindowsAzure.MobileServices;
using System.Net;

namespace TED.WindowsAzure.Unplugged.Mobile.RemoteStorage.AzureBlob.Storage
{
    public class WindowsAzureBlobStorage : IRemoteFileStorage
    {
        private static Tuple<string, string> _storageAccessKey = null;
        private IUnpluggedMobileServiceClient _client;

        public WindowsAzureBlobStorage(IUnpluggedMobileServiceClient client)
        {
            _client = client;
        }

        public async Task DownloadToStreamAsync(RemoteFile instance, IOutputStream target)
        {
            // Create an OperationContext to track the request 
            var downloadBlobContext = new OperationContext();

            bool retry = true;
            bool renewSAS = false;
            int retries = 0;

            while (retry)
            {
                int? exceptionResult = null;


                try
                {
                    Tuple<string, string> sasKey =
                        await GetStorageAccessKeyAsync(_client, instance.FolderName, renewSAS);

                    CloudBlobClient client = new CloudBlobClient(new Uri(sasKey.Item2),
                        new StorageCredentials(sasKey.Item1));

                    AccessCondition accessCondition = AccessCondition.GenerateEmptyCondition();

                    ICloudBlob blobFile =
                        await
                            client.GetBlobReferenceFromServerAsync(instance.Uri, accessCondition,
                                new BlobRequestOptions(), downloadBlobContext);

                    await
                        blobFile.DownloadToStreamAsync(target, accessCondition, new BlobRequestOptions(),
                            downloadBlobContext);
                }
                catch (HttpRequestException ex)
                {
                    // error connecting 
                    // rethrow the exception
                    throw new SyncConnectionException(ex.Message, ex.InnerException);
                }
                catch (MobileServiceInvalidOperationException ex)
                {
                    // error accessing the Access Key information
                    throw new SyncException(ex.Message, ex.InnerException);
                }
                catch (Exception ex)
                {
                    // error connecting to the blob stroage
                    // what exception was thrown???
                    exceptionResult = ex.HResult;
                }
                finally
                {
                    retry = false;
                }

                if(exceptionResult.HasValue)
                {
                    if(downloadBlobContext.LastResult != null)
                    {
                        if(downloadBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.Forbidden))
                        {
                            retries++;

                            // the user may legitimately be forbidden to access the storage account 
                            if(retries < 3)
                            {
                                retry = true;
                                renewSAS = true;
                            }
                            else
                            {
                                throw new SyncConnectionException("User is forbidden to access storage account");
                            }
                        }
                        else
                        {
                            // Display a detailed error to the user. Alternatively, you could inspect the extended error information returned by the server to provide some recover logic here. 
                            string message = string.Format("{0}:{1}", downloadBlobContext.LastResult.HttpStatusMessage,
                                downloadBlobContext.LastResult.HttpStatusCode);
                            throw new InvalidOperationException(message);
                        }
                    }
                    else
                    {
                        // there was an error so do not continue and throw an internal server error
                        throw new InvalidOperationException("Internal Server Error");
                    }
                }
            }
        }

        public async Task UploadFromStreamAsync(RemoteFile instance, IInputStream source)
        {
            // Create an OperationContext to track the request 
            var uploadBlobContext = new OperationContext();

            bool retry = true;
            bool renewSAS = false;
            int retries = 0;

            while (retry)
            {
                int? exceptionResult = null;

                try
                {
                    Tuple<string, string> sasKey =
                        await GetStorageAccessKeyAsync(_client, instance.FolderName, renewSAS);

                    CloudBlobClient client = new CloudBlobClient(new Uri(sasKey.Item2),
                        new StorageCredentials(sasKey.Item1));

                    var containerUri = new Uri(string.Format("{0}/{1}", sasKey.Item2, instance.FolderName));

                    CloudBlobContainer container = client.GetContainerReference(instance.FolderName);

                    CloudBlockBlob blobFile = container.GetBlockBlobReference(instance.FileName);

              
                    // this ensures the file will only be uploaded if the eTag on the client matches the eTag on the Server
                    // i.e. the server has not been modified since the last pull
                    AccessCondition accessCondition = AccessCondition.GenerateIfMatchCondition(instance.ETag);

                    var blobRequestOptions = new BlobRequestOptions
                    {
                        MaximumExecutionTime = new TimeSpan(0, 30, 0),
                        ServerTimeout = new TimeSpan(0, 30, 0)
                    };

                    await blobFile.UploadFromStreamAsync(source, accessCondition, blobRequestOptions, uploadBlobContext);
                }
                catch (HttpRequestException ex)
                {
                    // error connecting 
                    // rethrow the exception
                    throw new SyncConnectionException(ex.Message, ex.InnerException);
                }
                catch (MobileServiceInvalidOperationException ex)
                {
                    // error accessing the Access Key information
                    throw new SyncException(ex.Message, ex.InnerException);
                }
                catch (Exception ex)
                {
                    // error connecting to the blob stroage
                    // what exception was thrown???
                    exceptionResult = ex.HResult;
                }
                finally
                {
                    retry = false;
                }

                if(exceptionResult.HasValue)
                {
                    if(uploadBlobContext.LastResult != null)
                    {
                        if(uploadBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.Forbidden))
                        {
                            retries++;

                            // the user may legitimately be forbidden to access the storage account 
                            if(retries < 3)
                            {
                                retry = true;
                                renewSAS = true;
                            }
                            else
                            {
                                throw new SyncConnectionException("User is forbidden to access storage account");
                            }
                        }
                        else
                        {
                            // Display a detailed error to the user. Alternatively, you could inspect the extended error information returned by the server to provide some recover logic here. 
                            string message = string.Format("{0}:{1}", uploadBlobContext.LastResult.HttpStatusMessage,
                                uploadBlobContext.LastResult.HttpStatusCode);
                            throw new InvalidOperationException(message);
                        }
                    }
                    else
                    {
                        // there was an error so do not continue and throw an internal server error
                        throw new InvalidOperationException("Internal Server Error");
                    }
                }
            }
        }

        public async Task DeleteAsync(RemoteFile instance)
        {
            if(instance.Uri == null)
                return;

            // Create an OperationContext to track the request 
            var deleteBlobContext = new OperationContext();
            bool retry = true;
            int retries = 0;

            while (retry)
            {
                int? exceptionResult = null;


                // Found that checking to see if the blob existed before trying to download was a waste of time as the
                // ExistsAsync method failed when using a SAS key??? It was also an extra hop to the network so try download and catch then continue
                try
                {
                    Tuple<string, string> sasKey = await GetStorageAccessKeyAsync(_client, instance.FolderName);

                    CloudBlobClient client = new CloudBlobClient(new Uri(sasKey.Item2),
                        new StorageCredentials(sasKey.Item1));

                    ICloudBlob blobFile = await client.GetBlobReferenceFromServerAsync(instance.Uri);

                    // this ensures the file will only be uploaded if the eTag on the client matches the eTag on the Server
                    // i.e. the server has not been modified since the last pull
                    AccessCondition accessCondition = AccessCondition.GenerateIfMatchCondition(instance.ETag);

                    await
                        blobFile.DeleteIfExistsAsync(new DeleteSnapshotsOption(), accessCondition,
                            new BlobRequestOptions(), deleteBlobContext);
                }
                catch (HttpRequestException ex)
                {
                    // error connecting 
                    // rethrow the exception
                    throw new SyncConnectionException(ex.Message, ex.InnerException);
                }
                catch (MobileServiceInvalidOperationException ex)
                {
                    // error accessing the Access Key information
                    throw new SyncException(ex.Message, ex.InnerException);
                }
                catch (Exception ex)
                {
                    // error connecting to the blob stroage
                    // what exception was thrown???
                    exceptionResult = ex.HResult;
                }
                finally
                {
                    retry = false;
                }


                if(exceptionResult.HasValue)
                {
                    if(deleteBlobContext.LastResult != null)
                    {
                        if(deleteBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.Forbidden))
                        {
                            retries++;

                            if(retries < 3)
                            {
                                retry = true;
                            }
                            else
                            {
                                throw new SyncConnectionException("User is forbidden to access storage account");
                            }
                        }
                        else
                        {
                            // if the file does not exist on the server then it has been removed by another user?? 
                            // as it is already deleted don't throw an error
                            if(!deleteBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.NotFound))
                            {
                                string message = string.Format("{0}:{1}", deleteBlobContext.LastResult.HttpStatusMessage,
                                    deleteBlobContext.LastResult.HttpStatusCode);
                                throw new InvalidOperationException(message);
                            }
                        }
                    }
                    else
                    {
                        // there was an error so do not continue and throw an internal server error
                        throw new InvalidOperationException("Internal Server Error");
                    }
                }
            }
        }

        /// <summary>
        ///     if the blob file exists then return a new instance of RemoteFile, otherwise return null
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public async Task<RemoteFile> LookupAsync(string fileName, string folderName)
        {
            RemoteFile result = null;
            ICloudBlob blob = null;

            // Create an OperationContext to track the request 
            var lookupBlobContext = new OperationContext();

            bool retry = true;
            bool renewSAS = false;
            int retries = 0;

            while (retry)
            {
                int? exceptionResult = null;

                try
                {
                    Tuple<string, string> sasKey = await GetStorageAccessKeyAsync(_client, folderName, renewSAS);
                    var blobClient = new CloudBlobClient(new Uri(string.Format("{0}?{1}", sasKey.Item2, sasKey.Item1)));
                    CloudBlobContainer blobContainer = blobClient.GetContainerReference(folderName);

                    AccessCondition accessCondition = AccessCondition.GenerateEmptyCondition();

                    blob =
                        await
                            blobContainer.GetBlobReferenceFromServerAsync(fileName, accessCondition,
                                new BlobRequestOptions(), lookupBlobContext);
                }
                catch (HttpRequestException ex)
                {
                    // error connecting 
                    // rethrow the exception
                    throw new SyncConnectionException(ex.Message, ex.InnerException);
                }
                catch (MobileServiceInvalidOperationException ex)
                {
                    // error accessing the Access Key information
                    throw new SyncException(ex.Message, ex.InnerException);
                }
                catch (Exception ex)
                {
                    // error connecting to the blob stroage
                    // what exception was thrown???
                    exceptionResult = ex.HResult;
                }
                finally
                {
                    retry = false;
                }

                if(exceptionResult.HasValue)
                {
                    if(lookupBlobContext.LastResult != null)
                    {
                        if(lookupBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.Forbidden))
                        {
                            retries++;

                            if(retries < 3)
                            {
                                retry = true;
                                renewSAS = true;
                            }
                            else
                            {
                                throw new SyncConnectionException("User is forbidden to access storage account");
                            }
                        }
                        else
                        {
                            // if the file is not found then do not throw an exception, rather carry on
                            if(!lookupBlobContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.NotFound))
                            {
                                // Display a detailed error to the user. Alternatively, you could inspect the extended error information returned by the server to provide some recover logic here. 
                                string message = string.Format("{0}:{1}", lookupBlobContext.LastResult.HttpStatusMessage,
                                    lookupBlobContext.LastResult.HttpStatusCode);
                                throw new InvalidOperationException(message);
                            }
                        }
                    }
                    else
                    {
                        // there was an error so do not continue and throw an internal server error
                        throw new InvalidOperationException("Internal Server Error");
                    }
                }
            }

            result = CreateInstance(blob, folderName);

            if(blob == null)
                result.FileName = fileName;


            return result;
        }

        public async Task<IEnumerable<RemoteFile>> ReadAsync(string folderName)
        {
            var result = new List<ICloudBlob>();
            // Create an OperationContext to track the request 
            var readBlobsContext = new OperationContext();

            bool retry = true;
            bool renewSAS = false;
            int retries = 0;

            while (retry)
            {
                int? exceptionResult = null;
                try
                {
                    Tuple<string, string> sasKey = await GetStorageAccessKeyAsync(_client, folderName, renewSAS);

                    var blobClient = new CloudBlobClient(new Uri(string.Format("{0}?{1}", sasKey.Item2, sasKey.Item1)));
                    CloudBlobContainer blobContainer = blobClient.GetContainerReference(folderName);

                    var token = new BlobContinuationToken();

                    do
                    {
                        BlobResultSegment response = await blobContainer.ListBlobsSegmentedAsync(prefix: "",
                            useFlatBlobListing: true,
                            blobListingDetails: BlobListingDetails.All,
                            maxResults: 10000,
                            currentToken: token,
                            operationContext: readBlobsContext,
                            options: null);

                        result.AddRange(response.Results.OfType<ICloudBlob>());

                        token = response.ContinuationToken;
                    } while (token != null);
                }
                catch (HttpRequestException ex)
                {
                    // error connecting 
                    // rethrow the exception
                    throw new SyncConnectionException(ex.Message, ex.InnerException);
                }
                catch (MobileServiceInvalidOperationException ex)
                {
                    // error accessing the Access Key information
                    throw new SyncException(ex.Message, ex.InnerException);
                }
                catch (Exception ex)
                {
                    // error connecting to the blob stroage
                    // what exception was thrown???
                    exceptionResult = ex.HResult;
                }
                finally
                {
                    retry = false;
                }

                if(exceptionResult.HasValue)
                {
                    if(readBlobsContext.LastResult.HttpStatusCode.Equals((int) HttpStatusCode.Forbidden))
                    {
                        retries++;

                        if(retries < 3)
                        {
                            retry = true;
                            renewSAS = true;
                        }
                        else
                        {
                            throw new SyncConnectionException("User is forbidden to access storage account");
                        }
                    }
                    else
                    {
                        // Display a detailed error to the user. Alternatively, you could inspect the extended error information returned by the server to provide some recover logic here. 
                        string message = string.Format("{0}:{1}", readBlobsContext.LastResult.HttpStatusMessage,
                            readBlobsContext.LastResult.HttpStatusCode);
                        throw new InvalidOperationException(message);
                    }
                }
            }

            var list = new List<RemoteFile>();

            foreach (ICloudBlob b in result)
            {
                list.Add(CreateInstance(b, folderName));
            }

            return list;
        }

        private static async Task<Tuple<string, string>> GetStorageAccessKeyAsync(IUnpluggedMobileServiceClient client,
            string folderName, bool renew = false)
        {
            if(_storageAccessKey == null || renew == true)
            {
                Dictionary<string, string> result =
                    await client.InvokeApiAsync<Dictionary<string, string>>("storageaccess", HttpMethod.Get,
                        new Dictionary<string, string>()
                        {
                            {"containername", folderName}
                        });

                _storageAccessKey = new Tuple<string, string>(result["sasKey"], result["storageUrl"]);
            }

            return _storageAccessKey;
        }

        private RemoteFile CreateInstance(ICloudBlob blobFile, string folderName)
        {
            // create a remoteFolder first
            RemoteFolder folder = new RemoteFolder(this, folderName);

            RemoteFile remoteFile = new RemoteFile(folder);

            if(blobFile != null)
            {
                remoteFile.FileName = blobFile.GetFileName();
                remoteFile.Uri = blobFile.Uri;
                remoteFile.Exists = true;
                remoteFile.ETag = blobFile.Properties.ETag;
            }

            return remoteFile;
        }
    }
}